﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Common;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using System.Runtime.Serialization.Formatters.Binary;

namespace SIGReceiver
{
    /// <summary>
    /// Class that implements the SIG receiver protocol
    /// </summary>
    public class SelectiveRepeat
    {        
        private int _packetsToReturn = 0;
        private bool _resetDetected;
        private long _packetsToSend = 0;
        private Queue<SIGPacket> _returnBuffer = new Queue<SIGPacket>();
        private MemoryStream _toSendStream;
        private bool timedOut = false;
        private List<WindowElement> _window = new List<WindowElement>();
        private Timer _globalTimer;
        private int _globalCounter, _packetTimeout, _globalTimeout;
        private int _windowSize = 0;
        UdpClient _dataChannel, _ACKConnection;

        UdpState s;

        IPEndPoint _ep = null;

        BinaryFormatter _serializer = new BinaryFormatter();

        /// <summary>
        /// Creates an instance of the SIG protocol
        /// </summary>
        /// <param name="serverName">Name (or IP) of server to connect to</param>
        /// <param name="localPort">Port on server to connect to</param>
        public SelectiveRepeat(Int32 localPort)
        {
            try
            {
                _dataChannel = new UdpClient(new IPEndPoint(IPAddress.Any, localPort));
                _ACKConnection = new UdpClient(new IPEndPoint(IPAddress.Any, localPort + 1));                      
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            _packetTimeout = 1;
            _globalTimeout = 20;

            _windowSize = 5;
        }

        /// <summary>
        /// Sends a buffer over the network.
        /// </summary>
        /// <param name="buffer">The buffer to send</param>
        /// <returns>Number of bytes sent</returns>
        public byte[] receiveData()
        {
            _window.Capacity = _windowSize;
            for (int i = 0; i < _windowSize -1 ; i++)
                _window.Add(null);

            //Prepare receiving connection and handler
            UdpState s = new UdpState();
            s.e = new IPEndPoint(IPAddress.Any, 0);
            s.u = _dataChannel;

            //Console.WriteLine("listening for messages");                
            _dataChannel.BeginReceive(new AsyncCallback(UDPReceiveCallback), s);

            UdpState sACK = new UdpState();
            sACK.e = new IPEndPoint(IPAddress.Any, 0);
            sACK.u = _ACKConnection;

            _ACKConnection.BeginReceive(new AsyncCallback(ACKCallback), sACK);

            _globalTimer = new Timer(TimerCallback, s, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);

            //Send ACKs initially            

            while (!timedOut && _packetsToReturn == 0)
            {
                //Nothing to do, it is all taken care of by the UDP handler and the timer handler.                
                Thread.Sleep(100);
            }
            

            return makeBuffer(_returnBuffer);
        }

        private byte[] makeBuffer(Queue<SIGPacket> returnBuffer)
        {
            MemoryStream ms = new MemoryStream();

            foreach (SIGPacket packet in returnBuffer)
                ms.Write(packet.data, 0, packet.data.Length);

            return ms.ToArray();
        }
    

        private void UDPReceiveCallback(IAsyncResult ar)
        {
            Console.WriteLine("UDP Callback triggered");

            _globalCounter = 0;

            IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
            UdpClient u = ((UdpState)ar.AsyncState).u;
            //TODO: Error checking ?             
            if (!u.Equals(_dataChannel))
                return;
       

            Byte[] receiveBytes = u.EndReceive(ar, ref ep);            

            //TODO: should have a function for this
            SIGPacket packet = (SIGPacket)_serializer.Deserialize(new MemoryStream(receiveBytes));
            Console.WriteLine("Received packet with SEQ no.: " + packet.seqNo.ToString());

         
            //Is it a reset packet? Not sure this is needed
            if (packet.flags == Flags.CLOSE)
            {
                CloseConnection();
                throw new Exception("Connection closed by sender");
            }       


            //Is it the final confirmation packet? If yes, we are done
            if (packet.flags == Flags.FIN)
            {
                WindowElement el = new WindowElement();
                packet.flags = Flags.ACK;
                el.packet = packet;

                sendACK(el);
                _globalTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
                //Not receiving anymore
                lock (_window)
                {
                    foreach (WindowElement elem in _window.AsEnumerable())
                    {
                        SIGPacket myPacket = elem.packet;
                        _returnBuffer.Enqueue(myPacket);
                    }
                }
                //Write everything to the buffer, 
                _packetsToReturn = _returnBuffer.Count;

                return; //From here on we are NOT receiving datagrams anymore
            }
            //Is it the first packet? If yes, accept it
            if (packet.seqNo == 1)
            {
                WindowElement el = new WindowElement();
                el.packet = packet;
                el.timerEnabled = true;
                _window.Insert(0,el);
                sendACKs();
            }            


            if (packet.flags == Flags.NONE && packet.seqNo != 1 && _window.Count != 0)
            {
                //Is the SEQ number a valid one?
                //TODO: Wraparound for end of sequence issues
                //TODO: Make _window to List

                
                
                //Valid packet, in order
                //if (packet.seqNo == _window.Last().packet.seqNo + 1)
                {
                    //TODO:CAREFUL with enqueueing, if out of order
                    WindowElement el = new WindowElement();
                    el.packet = packet;
                    el.timerEnabled = true;              
                    
                    _window[_windowSize - (packet.seqNo % _windowSize) -1] = el;

                    //Write data to buffer to be returned
                    if (_window.Count == _windowSize + 1)
                    {
                        _returnBuffer.Enqueue(_window.Last().packet);
                    }

                    sendACKs();
                }                
            }

            //Re-enable receiving
            UdpState s = new UdpState();
            s.e = new IPEndPoint(IPAddress.Any, 0);
            s.u = _dataChannel;
            //Console.WriteLine("listening for messages");                
            _dataChannel.BeginReceive(new AsyncCallback(UDPReceiveCallback), s);
        }

        private void ACKCallback(IAsyncResult ar)
        {
            IPEndPoint ep = (IPEndPoint)(_ACKConnection.Client.LocalEndPoint);
            _ACKConnection.EndReceive(ar, ref ep);
            _ACKConnection.Connect(ep);
        }

        public void CloseConnection()
        {
            //Send an ACK packet for the RST?
            _globalTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
            _globalTimer.Dispose();

            //_dataChannel.EndReceive(null, ref _ep);
            _dataChannel.Close();


            //GC.Collect(2);
            //GC.WaitForPendingFinalizers();
        }

        public void TimerCallback(Object stateInfo)
        {
            Console.WriteLine("Receiver Timer Callback triggered at " + DateTime.Now.Second.ToString());
            _globalTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);

            _globalCounter++;

            lock (_window)
            {
                foreach (WindowElement el in _window.AsEnumerable())
                {
                    if (el.timerEnabled)
                        el.incTimer();
                }
            }
            
            sendACKs();
            _globalTimer.Change(1000, 1000);

            if (_globalCounter == _globalTimeout)
            {
                Console.WriteLine("Connection timed out");
                CloseConnection();
            }
        }


        public void sendACKs()
        {
            lock (this)
            {
                //Send packets if timedout or ACKed
                lock (_window)
                {
                    foreach (WindowElement el in _window.AsEnumerable())
                    {
                        if (el != null)
                            if (!el.acked || el.getTimerCounter() >= _packetTimeout)
                            {
                                if (el.getTimerCounter() >= _packetTimeout)
                                    Console.WriteLine("Packet timed out");

                                sendACK(el);
                                Thread.Sleep(1);
                            }
                    }
                }
            }
            return ;
        }

        private void sendACK(WindowElement element)
        {         
            SIGPacket packet = new SIGPacket();
            packet.flags = Flags.ACK;
            packet.seqNo = element.packet.seqNo;

            sendPacket(packet);
            Console.WriteLine("ACKed packet " + element.packet.seqNo.ToString());

            element.acked = true;
            element.resetTimer();
            element.timerEnabled = true;
        }

        private void sendPacket(SIGPacket packet)
        {
            MemoryStream stream = new MemoryStream();
            _serializer.Serialize(stream, packet);

            byte[] sendBytes = stream.ToArray();

            //Console.WriteLine(_ACKConnection.Client.RemoteEndPoint.ToString());
            _ACKConnection.Send(sendBytes, sendBytes.Length);
            
        }
    }
}
